#region References

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
using SpoolFileLibrary.Reader.Winspool;

#endregion

namespace SpoolFileLibrary.Reader.Emf {
    internal class EmfSpoolFile : ISpoolFile {
        private readonly BinaryReader binaryReader;
        private readonly IList<IPage> pages;
        private DeviceMode deviceMode;

        public EmfSpoolFile(Stream stream) {
            if(stream == null) {
                throw new ArgumentNullException("stream");
            }
            stream.Seek(0, SeekOrigin.Begin);
            this.binaryReader = new BinaryReader(stream, Encoding.Unicode);
            this.pages = new List<IPage>();
        }

        #region ISpoolFile Members

        public bool Parse(bool parsePages, int scaleFactor) {
            HeaderRecord headerRecord = this.ParseHeaderRecord();
            List<DataRecord> dataRecords = this.ParseDataRecords();
            if(parsePages) {
                for(int i = 0; i < dataRecords.Count; i++) {
                    var pageContentRecord = dataRecords[i] as PageContentRecord;
                    if(pageContentRecord != null) {
                        var memoryStream = new MemoryStream(pageContentRecord.EmfMetaFile);
                        var metaFile = new Metafile(memoryStream);
                        Image image = metaFile.GetThumbnailImage(metaFile.Width/scaleFactor, metaFile.Height/scaleFactor, null, IntPtr.Zero);
                        var emfPage = new EmfPage(image, image.Size);
                        this.pages.Add(emfPage);
                    }
                    var devmodeRecord = dataRecords[i] as EMRI_DEVMODERecord;
                    if(devmodeRecord != null) {
                        this.deviceMode = devmodeRecord.Devmode;
                    }
                }
            }
            return true;
        }

        public SpoolFileFormat SpoolFileFormat {
            get { return Reader.SpoolFileFormat.EMF; }
        }

        public PrintjobLanguage PrintjobLanguage {
            get { return PrintjobLanguage.UNKNOWN; }
        }

        public bool Parse() {
            return this.Parse(false, 0);
        }

        public IList<IPage> Pages {
            get { return this.pages; }
        }

        public int PageCount {
            get { return this.pages.Count; }
        }

        public int Copies {
            get { return this.deviceMode.Copies; }
        }

        public bool Color {
            get { return this.deviceMode.Color; }
        }

        public int PaperLength {
            get { return this.deviceMode.PaperLength; }
        }

        public int PaperWidth {
            get { return this.deviceMode.PaperWidth; }
        }

        public bool Landscape {
            get { return this.deviceMode.Orientation == Orientation.Landscape; }
        }

        public IDeviceMode DeviceMode {
            get { return this.deviceMode; }
        }

        public byte[] RawData {
            get { return null; }
        }
        
        #endregion

        private List<DataRecord> ParseDataRecords() {
            var dataRecords = new List<DataRecord>();
            while(true) {
                long pos = this.binaryReader.BaseStream.Position;
             
                bool eof = (pos == this.binaryReader.BaseStream.Length);
                if(eof) {
                    break;
                }
                uint ulID = this.binaryReader.ReadUInt32();
                if(ulID == 0x00000000) {
                    //Don't know why. Sometimes 0x0 is too early.
                    //So we need read it again.
                    ulID = this.binaryReader.ReadUInt32();
                }
                var dataRecord = new DataRecord();
                dataRecord.ulID = ulID;
                dataRecord.cjSize = this.binaryReader.ReadUInt32();
                if(!Enum.IsDefined(typeof(RecordType), dataRecord.ulID)) {
                    this.binaryReader.ReadBytes((int)dataRecord.cjSize); // Read unknown data block
                    continue;
                }
                switch(dataRecord.ulID) {
                    case (uint) RecordType.EMRI_METAFILE: {
                        var pageContentRecord = new PageContentRecord(dataRecord);
                        pageContentRecord.EmfMetaFile = this.binaryReader.ReadBytes((int) pageContentRecord.cjSize);
                        dataRecords.Add(pageContentRecord);
                    }
                        break;
                    case (uint) RecordType.EMRI_ENGINE_FONT:
                        break;
                    case (uint) RecordType.EMRI_DEVMODE: {
                        var devmodeRecord = new EMRI_DEVMODERecord(dataRecord);
                        byte[] buffer = this.binaryReader.ReadBytes((int) devmodeRecord.cjSize);
                        this.deviceMode = new DeviceMode(new DEVMODE(buffer));
                        devmodeRecord.Devmode = this.deviceMode;
                        dataRecords.Add(devmodeRecord);
                    }
                        break;
                    case (uint) RecordType.EMRI_TYPE1_FONT:
                        break;
                    case (uint) RecordType.EMRI_PRESTARTPAGE:
                        break;
                    case (uint) RecordType.EMRI_DESIGNVECTOR:
                        break;
                    case (uint) RecordType.EMRI_SUBSET_FONT:
                        break;
                    case (uint) RecordType.EMRI_DELTA_FONT:
                        break;
                    case (uint) RecordType.EMRI_FORM_METAFILE:
                        break;
                    case (uint) RecordType.EMRI_BW_METAFILE:
                        break;
                    case (uint) RecordType.EMRI_BW_FORM_METAFILE:
                        break;
                    case (uint) RecordType.EMRI_METAFILE_DATA: {
                        var pageContentRecord = new PageContentRecord(dataRecord);
                        pageContentRecord.EmfMetaFile = this.binaryReader.ReadBytes((int) dataRecord.cjSize);
                        dataRecords.Add(pageContentRecord);
                    }
                        break;
                    case (uint) RecordType.EMRI_METAFILE_EXT:
                    case (uint) RecordType.EMRI_BW_METAFILE_EXT: {
                        var pageOffsetRecord = new PageOffsetRecord(dataRecord);
                        var offsets = new ulong[pageOffsetRecord.cjSize/sizeof(ulong)];
                        for(int i = 0; i < offsets.Length; i++) {
                            offsets[i] = this.binaryReader.ReadUInt64();
                        }
                        dataRecords.Add(pageOffsetRecord);
                    }
                        break;
                    case (uint) RecordType.EMRI_ENGINE_FONT_EXT:
                        break;
                    case (uint) RecordType.EMRI_TYPE1_FONT_EXT:
                        break;
                    case (uint) RecordType.EMRI_DESIGNVECTOR_EXT:
                        break;
                    case (uint) RecordType.EMRI_SUBSET_FONT_EXT:
                        break;
                    case (uint) RecordType.EMRI_DELTA_FONT_EXT:
                        break;
                    case (uint) RecordType.EMRI_PS_JOB_DATA:
                        break;
                    case (uint) RecordType.EMRI_EMBED_FONT_EXT:
                        break;
                }
            }
            return dataRecords;
        }

        private HeaderRecord ParseHeaderRecord() {
            var headerRecord = new HeaderRecord();
            headerRecord.dwVersion = this.binaryReader.ReadUInt32();
            headerRecord.cjSize = this.binaryReader.ReadUInt32();
            headerRecord.dpszDocName = this.binaryReader.ReadUInt32();
            headerRecord.dpszOutput = this.binaryReader.ReadUInt32();

            if(headerRecord.dpszDocName != 0) {
                this.binaryReader.BaseStream.Seek(headerRecord.dpszDocName, SeekOrigin.Begin);
                var docNameStringBuilder = new StringBuilder();
                while(this.binaryReader.PeekChar() != 0) {
                    docNameStringBuilder.Append(this.binaryReader.ReadChar());
                }
                headerRecord.DocName = docNameStringBuilder.ToString();
            }
            this.Ensure32BitAlignment();
            if(headerRecord.dpszOutput != 0) {
                this.binaryReader.BaseStream.Seek(headerRecord.dpszOutput, SeekOrigin.Begin);
                var outputDeviceStringBuilder = new StringBuilder();
                while(this.binaryReader.PeekChar() != 0) {
                    outputDeviceStringBuilder.Append(this.binaryReader.ReadChar());
                }
                headerRecord.OutputDevice = outputDeviceStringBuilder.ToString();
            }
            this.Ensure32BitAlignment();
            this.EnsurePosition(headerRecord.cjSize);
            return headerRecord;
        }

        private void EnsurePosition(uint size)
        {
            this.binaryReader.BaseStream.Seek(size, SeekOrigin.Begin);
        }

        private void Ensure32BitAlignment() {
            long pos = this.binaryReader.BaseStream.Position;
            long currentAlignment = pos%4;
            if(currentAlignment == 0) {
                return;
            }
            this.binaryReader.BaseStream.Seek(currentAlignment, SeekOrigin.Current);
        }
    }
}